package cn.xiaopengstack.aop;

import cn.xiaopengstack.types.annotations.DCCValue;
import cn.xiaopengstack.types.annotations.RateLimiterAccessInterceptor;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.util.concurrent.RateLimiter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * @author jiangyangang
 * 限流切面
 */
@Slf4j
@Aspect
@Component
public class RateLimiterAOP {

    @DCCValue("rateLimiterSwitch:close")
    private String rateLimiterSwitch;

    // 个人限频记录1分钟
    private final Cache<String, RateLimiter> loginRecord = CacheBuilder.newBuilder()
            .expireAfterWrite(1, TimeUnit.MINUTES)
            .build();

    // 个人限频黑名单24h - 分布式业务场景，可以记录到 Redis 中
    private final Cache<String, Long> blacklist = CacheBuilder.newBuilder()
            .expireAfterWrite(24, TimeUnit.HOURS)
            .build();

    private final static String BLACK_LIST_CACHE_KEY = "black_list_cache:";
    private final static String LOGIN_LIST_CACHE_KEY = "login_list_cache:";


    @Pointcut("@annotation(cn.xiaopengstack.types.annotations.RateLimiterAccessInterceptor)")
    public void aopPoint(){};

    @Around("aopPoint() && @annotation(rateLimiterAccessInterceptor)")
    public Object doRouter(ProceedingJoinPoint jp, RateLimiterAccessInterceptor rateLimiterAccessInterceptor) throws Throwable{
        if (StringUtils.isNoneBlank(rateLimiterSwitch) && "close".equals(rateLimiterSwitch)){
            return jp.proceed();
        }
        // 获取Key
        String key = rateLimiterAccessInterceptor.key();
        if (StringUtils.isBlank(key)) {
            log.warn("获取限流|降级字段失败, sign={}", jp.getSignature().getName());
            return jp.proceed();
        }


        // 获取key值
        String keyAttr = getAttrValue(key, jp.getArgs());
        if (StringUtils.isBlank(keyAttr)) {
            log.warn("获取限流|降级字段值, key={}, sign={}", key, jp.getSignature().getName());
            return jp.proceed();
        }

        String blackListCacheKey = BLACK_LIST_CACHE_KEY + keyAttr;
        String logCacheKey = LOGIN_LIST_CACHE_KEY + keyAttr;

        // 黑名单拦截
        if (!"all".equals(keyAttr) &&
                rateLimiterAccessInterceptor.blacklistCount() > 0 &&
                Objects.nonNull(blacklist.getIfPresent(blackListCacheKey)) &&
                blacklist.getIfPresent(blackListCacheKey) > rateLimiterAccessInterceptor.blacklistCount()) {
            log.info("限流-黑名单拦截(24h)：{}", keyAttr);
            return fallbackMethodResult(jp, rateLimiterAccessInterceptor.fallbackMethod());
        }

        // 获取限流 -> Guava 缓存1分钟
        RateLimiter rateLimiter = loginRecord.getIfPresent(logCacheKey);
        if (null == rateLimiter) {
            rateLimiter = RateLimiter.create(rateLimiterAccessInterceptor.permitsPerSecond());
            loginRecord.put(logCacheKey, rateLimiter);
        }

        // 限流拦截
        if (!rateLimiter.tryAcquire()) {
            if (rateLimiterAccessInterceptor.blacklistCount() != 0) {
                if (null == blacklist.getIfPresent(keyAttr)) {
                    blacklist.put(blackListCacheKey, 1L);
                } else {
                    blacklist.put(blackListCacheKey, blacklist.getIfPresent(blackListCacheKey) + 1L);
                }
            }
            log.info("限流-超频次拦截：{}", keyAttr);
            return fallbackMethodResult(jp, rateLimiterAccessInterceptor.fallbackMethod());
        }

        // 返回结果
        return jp.proceed();
    }

    /**
     * 执行fallback方法
     * @param jp
     * @param fallbackMethod
     * @return
     */
    private Object fallbackMethodResult(ProceedingJoinPoint jp, String fallbackMethod) throws Exception {
        Signature sig = jp.getSignature();
        MethodSignature methodSignature = (MethodSignature) sig;
        Method method = jp.getTarget().getClass().getMethod(fallbackMethod, methodSignature.getParameterTypes());
        return method.invoke(jp.getThis(), jp.getArgs());
    }

    /**
     * 获取入参的指定值
     * @param attr
     * @param args
     * @return
     */
    private String getAttrValue(String attr, Object[] args) {
        if (args[0] instanceof String) {
            return args[0].toString();
        }

        String filedValue = null;
        for (Object arg : args) {
            try {
                if (StringUtils.isNotBlank(filedValue)) {
                    break;
                }
                // filedValue = BeanUtils.getProperty(arg, attr);
                // fix: 使用lombok时，uId这种字段的get方法与idea生成的get方法不同，会导致获取不到属性值，改成反射获取解决
                filedValue = String.valueOf(this.getValueByName(arg, attr));
            } catch (Exception e) {
                log.error("获取路由属性值失败 attr：{}", attr, e);
            }
        }
        return filedValue;
    }

    /**
     * 获取对象的特定属性值
     *
     * @param item 对象
     * @param name 属性名
     * @return 属性值
     * @author tang
     */
    private Object getValueByName(Object item, String name) {
        try {
            Field field = getFieldByName(item, name);
            if (field == null) {
                return null;
            }
            field.setAccessible(true);
            Object o = field.get(item);
            field.setAccessible(false);
            return o;
        } catch (IllegalAccessException e) {
            return null;
        }
    }

    /**
     * 根据名称获取方法，该方法同时兼顾继承类获取父类的属性
     *
     * @param item 对象
     * @param name 属性名
     * @return 该属性对应方法
     * @author tang
     */
    private Field getFieldByName(Object item, String name) {
        try {
            Field field;
            try {
                field = item.getClass().getDeclaredField(name);
            } catch (NoSuchFieldException e) {
                field = item.getClass().getSuperclass().getDeclaredField(name);
            }
            return field;
        } catch (NoSuchFieldException e) {
            return null;
        }
    }
}
